TCP Server 驱动 v3.2
该驱动内置了一个 tcp server 服务, 驱动启动后可以接收客户端的连接和数据, 同时也可以向客户端发送数据. 如果使用容器部署时, 需要将 tcp 服务端口开放.
注: 每个驱动实例只能添加一个模型
配置说明
以下内容为 tcp-server
驱动的配置项
server:
port: 9404 # TCP 监听的端口, 默认 9404
bufferSize: 4096 # 每个客户端连接的默认缓存冲区大小, 默认 4096
注:
tcp.bufferSize
为每个客户端连接的默认的接收缓存大小, 仅当未在模型配置中设置了有效值时生效, 如果模型配置中设置了有效值(大于 0)时则覆盖该配置. 缓存大小需要根据协议内容进行设置, 不能小于单个数据包长度的最大值, 否则无法正常工作. 建议设置为最大数据包长度的 3 倍.
数据处理流程说明
- 当接收到客户端发送的数据时, 会读取所有的发送数据并存放到
Buffer
中, 然后调用数据包拆分脚本
数据包拆分脚本
从Buffer
中提取完整的数据包信息或.
脚本说明
脚本语言: JavasScript ECMAScript 5.1
驱动使用时要求提供 数据包拆分脚本
, 数据处理脚本
和 指令处理脚本
3 个脚本函数来处理接收和发送数据过程中的协议和数据格式问题.
在脚本的上下文中内置了 Buffer
包, 可用于处理接收或发送二进制数据.
除此之外, 还内置了 lodash
, crypto-js
, moment
, xml-js
和 formulajs(Excel函数)
包.
注: 所有的脚本函数名必须为
**handler**
客户端对象
在 数据包拆分脚本
, 数据处理脚本
, 指令处理脚本
和 连接处理脚本
函数的参数中提供了 client
对象, 该对象为当前 TCP
连接客户端对象,
可以通过该对象实现向客户端发送数据的功能. 例如: 向客户端发送 ack
信息等.
该对象提供了以下函数:
register
用于注册设备.
接收到客户端发送的设备注册信息时, 在脚本函数中通过调用 client
对象中的 register
函数注册该设备, 驱动会将该设备与当前连接绑定.
当需要向设备发送指令时, 会通过该绑定关系向对应的连接发送数据.
如果协议未定义设备注册, 会向上次接收到该设备数据的连接发送. 详细信息见 设备与连接绑定关系说明
参数说明
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
array[string] | 字符串列表 | 设备标识列表 | ["d01", "d02"] |
返回值
string
或 undefined
如果参数不正确、部分设备标识错误或对应的设备不存在时返回 string
, 内容为错误说明.
如果所有设备注册成功, 返回 undefined
示例
function handler(client, buffer) {
client.register(["device01", "device02"]);
}
unregister
用于注销设备. 注销设备与连接的绑定关系, 参数及返回值说明同 register
示例
function handler(client, buffer) {
client.unregister(["device01", "device02"]);
}
send
用于向客户端发送数据.
例如: 在接收到数据时向客户端发送 ack
信息.
参数说明
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
data | Buffer | 发送的内容 | Buffer.from("hello") 代表要发送 "hello" |
返回值
string
或 undefined
如果参数不正确(字节数组为空或空数组)或发送失败则返回 string
内容为错误说明, 如果发送成功则返回 undefined
.
示例
function handler(client, buffer) {
// 向客户端发送 hello world
client.send(Buffer.from("hello world"));
}
getRemoteAddr
用于获取客户端地址信息.
参数说明
无
返回值
string
. 格式: IP:Port
. 例如: 192.0.2.1:25
示例
var remoteAddr = client.getRemoteAddr();
getRemoteIp
用于获取客户端 IP 地址信息.
参数说明
无
返回值
string
. 例如: 192.0.2.1
示例
var clientIp = client.getRemoteIp();
reportCommand
上报指令执行结果. 有些场景指令的执行结果反馈是异步的, 在指令发送后, 过一段时间才会收到响应报文,
此时可以在接收到响应报文时通过 client.reportCommand(...)
方法上报指令执行结果.
参数说明
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
serialNo | String | 平台指令序号 | "f160a24b-1780-89e7-cd48-b4c0073bd0fe" |
deviceId | String | 设备标识 | "ST10001" |
state | String | 状态标识 | 自定义状态标识, 例如: Success, Failed 等 |
result | String | 结果数据 | 有些指令可能有响应数据, 可以将结果数据保存在 result 字段中. 例如: 读取设备配置指令, 会返回设备配置信息 |
注:
serialNo
为平台在发送指令时生成, 会传入到指令处理脚本
中, 当指令执行结果为异步反馈时, 可以将serialNo
保存到设备上下文
中, 以便在后续reportCommand
使用
返回值
string
或 undefined
. 如果返回 undefined
则表示发送成功, 否则返回数据为错误原因.
示例
// 模拟指令处理脚本, 假设指令执行结果为异步反馈
function handler(client, serialNo, deviceId, commmand) {
// 获取设备上下文
const deviceContext = client.getDeviceContext(deviceId);
// 将平台指令序号保存到设备上下文中, 以便后面使用
deviceContext.put(command.name, {"serialNo": serialNo});
// 其它
}
// 数据处理脚本, 模拟收到指令执行结果
function handler(client, buffer) {
// 假设读取到的数据为指令执行结果
// 从数据中读取出设备标识
const deviceId = buffer.slice(4, 20).toString();
// 从数据中读取出指令标识
const commandName = buffer.slice(20, 30).toString();
// 批令执行成功标识
const state = buffer[30];
// 执行结果信息
const result = buffer.slice(31, 50).toString();
// 获取设备上下文
const deviceContext = client.getDeviceContext(deviceId);
// 从设备上下文中获取并删除指令信息
const command = deviceContext.getAndRemove(commandName);
// 发送指令执行结果
const sendResult = client.reportCommand(command.serialNo, deviceId, state == 0 ? 'SUCCESS' : 'FAILED', result);
if (!sendResult) {
console.log("批令发送失败:", sendResult);
}
}
getMediaFile
请求媒体库文件
参数说明
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
path | String | 文件路径 | 文件在媒体库的路径. 例如: /background/bg1.png 为目录 background 中的 bg1.png 文件 |
返回值
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
success | bool | 是否成功 | 请求是否成功. true: 成功, false: 失败 |
message | string | 信息 | 请求失败时为失败原因 |
data | object | 文件信息 | 请求成功时该字段存在 |
name | string | 文件名 | 文件名. 例如: bg1.png |
size | number | 文件大小 | 单位: 字节 |
data | Buffer | 文件内容 | Buffer中包含文件内的全部数据 |
示例
function handler(client) {
const response = client.getMediaFile("/background/bg1.png");
if (!response.success) {
console.log("请求媒体库文件失败:", response.message);
return;
}
// 文件信息
const fileInfo = response.data;
// 文件名
const filename = fileInfo.name;
// 文件大小
const fileSize = fileInfo.size;
// 文件内容
const fileData = fileInfo.data;
}
getMediaFileByURL
请求媒体库文件
参数说明
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
url | String | 文件 url | 文件在媒体库请求 url, 该信息一般由媒体库组件或附件组件获得. 例如: /core/fileServer/mediaLibrary/default/test/hello.txt |
注: url 中
default
为项目ID,test
为目录,hello.txt
为文件名.
返回值
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
success | bool | 是否成功 | 请求是否成功. true: 成功, false: 失败 |
message | string | 信息 | 请求失败时为失败原因 |
data | object | 文件信息 | 请求成功时该字段存在 |
name | string | 文件名 | 文件名. 例如: bg1.png |
size | number | 文件大小 | 单位: 字节 |
data | Buffer | 文件内容 | Buffer中包含文件内的全部数据 |
示例
function handler(client) {
const response = client.getMediaFileByURL("/core/fileServer/mediaLibrary/default/test/hello.txt");
if (!response.success) {
console.log("请求媒体库文件失败:", response.message);
return;
}
// 文件信息
const fileInfo = response.data;
// 文件名
const filename = fileInfo.name;
// 文件大小
const fileSize = fileInfo.size;
// 文件内容
const fileData = fileInfo.data;
}
uploadMediaFile
上传文件到媒体库
参数说明
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
filename | String | 文件名 | 例如: hello.txt |
catalog | String | 目录 | 文件上传到的目录, 支持多级目录. 例如: image/background |
action | String | 存在同名文件时的动作 | cover: 覆盖已有文件, rename: 新上传文件名后面自动加1 |
data | Buffer | 文件内容 | 文件的内容 |
返回值
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
success | bool | 是否成功 | 请求是否成功. true: 成功, false: 失败 |
message | string | 信息 | 请求失败时为失败原因 |
示例
function handler(client) {
// 上传 hello.txt 到 data/note 目录, 文件内容为 "hello world"
const response = client.uploadMediaFile("hello.txt", "data/note", "cover", Buffer.from("hello world"));
if (!response.success) {
console.log("上传文件失败:", response.message);
return;
}
}
deleteMediaFile
删除媒体库文件
参数说明
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
path | String | 文件路径 | 文件在媒体库的路径. 例如: /background/bg1.png 为目录 background 中的 bg1.png 文件 |
返回值
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
success | bool | 是否成功 | 请求是否成功. true: 成功, false: 失败 |
message | string | 信息 | 请求失败时为失败原因 |
示例
function handler(client) {
// 删除目录 background 中的 bg1.png 文件
const response = client.deleteMediaFile("/background/bg1.png");
if (!response.success) {
console.log("删除媒体库文件失败:", response.message);
return;
}
}
saveWorkTableRow
向工作表写入一条数据
参数说明
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
tableId | String | 工作表标识 | 例如: student |
row | object | 写入数据 | 该字段为 JSON 对象, 内容根据工作表定义填写. 例如: {"name": "小明", "age": 18} |
返回值
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
success | bool | 是否成功 | 请求是否成功. true: 成功, false: 失败 |
message | string | 信息 | 请求失败时为失败原因 |
data | string | 记录ID | 请求成功时, 为新增记录的的ID |
示例
function handler(client) {
// 删除目录 background 中的 bg1.png 文件
const response = client.saveWorkTableRow("student", {"name": "小明", "age": 18});
if (!response.success) {
console.log("写入数据失败:", response.message);
return;
}
// 新增记录ID
const rowId = response.data;
}
updateWorkTableRow
更新工作表中的数据
参数说明
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
tableId | String | 工作表标识 | 例如: student |
query | object | 过滤条件 | 该字段为 JSON 对象, 过滤出要更新哪些记录. 例如: {"name": "小明"} 更新 name 为 "小明" 的所有记录 |
row | object | 写入数据 | 该字段为 JSON 对象, 内容根据工作表定义填写. 例如: {"age": 19} 更新 age 字段的值为 19 |
返回值
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
success | bool | 是否成功 | 请求是否成功. true: 成功, false: 失败 |
message | string | 信息 | 请求失败时为失败原因 |
示例
function handler(client) {
// 更新 name 字段值为 "小明" 所有记录的 age 字段的值为 19
const response = client.updateWorkTableRow("student", {"name": "小明"}, {"age": 19});
if (!response.success) {
console.log("更新数据失败:", response.message);
return;
}
}
updateNodeById
根据资产标识更新资产数据
参数说明
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
nodeId | String | 资产标识 | 例如: ST10001 |
data | object | 更新内容 | 该字段为 JSON 对象, 要更新的内容. 例如: {"name": "小明", "age": 18} |
返回值
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
success | bool | 是否成功 | 请求是否成功. true: 成功, false: 失败 |
message | string | 信息 | 请求失败时为失败原因 |
示例
function handler(client) {
// 更新 name 字段值为 "小明" 所有记录的 age 字段的值为 19
const response = client.updateNodeById("ST10001", {"name": "小明", "age": 18});
if (!response.success) {
console.log("更新资产失败:", response.message);
return;
}
}
getContext
用于获取上下文对象, 可以在上下文中存储数据. 详细信息
注: 第一次调用的时候才会创建上下文
参数说明
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
contextId | String | 上下文标识 | "myContext" |
返回值
Object
. 上下文对象
示例
function handler(client) {
// 获取一个标识为 myContext 的上下文
var myContext = client.getContext("myContext");
// 使用客户端 IP 地址作为标识
var context = client.getContext(client.getRemoteIp());
}
getDeviceContext
用于获取设备上下文对象, 使用设备标识作为上下文标识, 可以在上下文中存储数据. 详细信息
与 getContext
不同的是, 当设备被删除后, 重启驱动时会自动清理被删除设备的上下文对象.
注: 第一次调用的时候才会创建上下文
参数说明
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
deviceId | String | 设备标识 | "ST10001" |
返回值
Object
. 上下文对象
示例
function handler(client) {
// 获取设备 ST10001 的上下文
var context = client.getDeviceContext("ST10001");
}
removeContext
删除上下文对象
参数说明
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
contextId | String | 上下文标识 | "myContext" |
返回值
无
示例
function handler(client) {
// 删除标识为 myContext 的上下文
client.removeContext("myContext");
// 删除以客户端 IP 地址作为标识的上下文
client.removeContext(client.getRemoteIp());
}
removeDeviceContext
删除设备上下文对象
参数说明
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
deviceId | String | 上下文标识 | "ST10001" |
返回值
无
示例
function handler(client) {
// 删除设备 ST10001 的上下文
client.removeDeviceContext("ST10001");
}
getContextIds
获取全部上下文标识(不包含设备上下文)
参数说明
无
返回值
String[]
示例
function handler(client) {
// 创建两个上下文
const context1 = client.getContext("context1");
const context2 = client.getContext("context2");
// 获取全部上下文标识 ["context1", "context2"]
const contextIds = client.getContextIds();
}
getDeviceContextIds
获取全部设备上下文标识(只包含设备上下文)
参数说明
无
返回值
String[]
示例
function handler(client) {
// 创建两个设备上下文
const context1 = client.getDeviceContext("ST10001");
const context2 = client.getDeviceContext("ST10002");
// 获取全部设备上下文标识 ["ST10001", "ST10002"]
const contextIds = client.getDeviceContextIds();
}
context
上下文对象, 用来存储数据, 上下文中的数据可以在不同的脚本中共享. 例如: 可以在 指令处理脚本中
写入数据,
然后从 数据处理脚本
中读取数据. 不同上下文彼此独立, 互不影响.
注: 可以根据需求创建多个上下文对象, 但是上下文对象以及上下文中的数据需要及时清理, 否则会造成
**OOM**
问题, 导致驱动程序崩溃.
put
用于向上下文中存储数据.
注: 存入的数据需要自行清理, 否则可能导致
OOM
等问题.
参数说明
参数名 | 参数类型 | 参数说明 |
---|---|---|
key | string | 数据项的 key |
value | any | 数据项的值 |
返回值
无
示例
function handler(client, request) {
// 获取或创建标识为 myContext 的上下文
const context = client.getContext("myContext");
// 向上下文中存储一个字符串
context.put("string", "this is a string");
// 向上下文中存储一个数值
context.put("number", 3.141);
// 向上下文中存储一个对象
context.put("object", {name: "张三", age: 18});
// 获取或创建设备 ST10001 的上下文
const deviceContext = client.getDeviceContext("ST10001");
// 向上下文中存储一个字符串
deviceContext.put("string", "this is a string");
// 向上下文中存储一个数值
deviceContext.put("number", 3.141);
// 向上下文中存储一个对象
deviceContext.put("object", {name: "张三", age: 18});
}
containsKey
判断上下文中是否存在指定的 key
参数说明
参数名 | 参数类型 | 参数说明 |
---|---|---|
key | string | 数据项的 key |
返回值
bool
. true
表示 key
存在, false
表示 key
不存在
示例
function handler(client) {
// 获取标识为 myContext 的上下文
const context = client.getContext("myContext");
// 向上下文中存储一个字符串
context.put("string", "this is a string");
// 返回 true
context.containsKey("string");
// 返回 false
context.containsKey("string1");
}
get
从上下文中获取指定的 key
对应的数据. 如果 key
不存在则返回 undefined
参数说明
参数名 | 参数类型 | 参数说明 |
---|---|---|
key | string | 数据项的 key |
返回值
any
或 undefined
. 返回 put
时写入的数据.
示例
function handler(client) {
// 获取标识为 myContext 的上下文
const context = client.getContext("myContext");
// 向上下文中存储一个字符串
context.put("string", "this is a string");
// 返回 "this is a string"
context.get("string");
// 返回 undefined
context.get("string1");
}
getAndRemove
从上下文中获取指定的 key
对应的数据并且在返回后 删除 该 key
. 如果 key
不存在则返回 undefined
.
注: 该函数返回后, 再使用
get
或getAndRemove
均返回undefined
.
参数说明
参数名 | 参数类型 | 参数说明 |
---|---|---|
key | string | 数据项的 key |
返回值
any
或 undefined
. 返回 put
时写入的数据.
示例
function handler(client) {
// 获取标识为 myContext 的上下文
const context = client.getContext("myContext");
// 向上下文中存储一个字符串
context.put("string", "this is a string");
// 返回 "this is a string"
context.getAndRemove("string");
// 返回 undefined
context.get("string");
// 返回 undefined
context.getAndRemove("string");
}
remove
从上下文中删除指定的 key
, 如果 key
不存在则不执行任何操作.
参数说明
参数名 | 参数类型 | 参数说明 |
---|---|---|
key | string | 数据项的 key |
返回值
无
示例
function handler(client) {
// 获取标识为 myContext 的上下文
const context = client.getContext("myContext");
// 向上下文中存储一个字符串
context.put("string", "this is a string");
// 数据被删除
context.remove("string");
// 不执行任何操作
context.remove("string");
}
内置函数
crc 校验
驱动中内置了 crc
对象, 可以在脚本中直接使用 crc
对象中的校验函数.
使用 checksum16 实现 crc16 校验
function handler() {
// 示例数据
const data = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9]);
// 多项式
const poly = 0x1234;
// 返回 uint16 的校验码
const checksum = crc.checksum16(data, poly);
// 使用内置函数, 将校验码转换为字节数组(大端字节序)
const checksumBytes = bigEndian.encodeUint16(checksum);
}
使用 checksum32 实现 crc32 校验
function handler() {
// 示例数据
const data = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9]);
// 多项式
const poly = 0xedb88320;
// 返回 uint32 的校验码
const checksum = crc.checksum32(data, poly);
// 使用内置函数, 将校验码转换为字节数组(大端字节序)
const checksumBytes = bigEndian.encodeUint32(checksum);
}
使用 checksum64 实现 crc64 校验
function handler() {
// 示例数据
const data = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9]);
// 多项式
const poly = 0xD800000000000000;
// 返回 uint64 的校验码
const checksum = crc.checksum64(data, poly);
// 使用内置函数, 将校验码转换为字节数组(大端字节序)
const checksumBytes = bigEndian.encodeUint64(checksum);
}
使用 checksumModbus 实现 modbus 协议 crc 校验
modbus
校验为 crc16
, 所以校验码类型为 uint16
. 另外, checksumModbus
不需要 poly
多项式参数.
function handler() {
// 示例数据
const data = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9]);
// 返回 uint16 的校验码
const checksum = crc.checksumModbus(data);
// 使用内置函数, 将校验码转换为字节数组(大端字节序)
const checksumBytes = bigEndian.encodeUint16(checksum);
}
数据包拆分脚本
该脚本用于处理接收数据过程中的半包和粘包问题, 该函数需要根据协议和数据格式判断接收到的字节数组中是否包含完整的数据包, 如果包含了完整数据包则返回完据包的起始位置和长度. 或者当数据包有错误时丢掉该数据包或有问题部分的数据.
如果字节数组中不包含完整的数据包, 直接返回 undefined
表示需要从客户端接收更多的数据.
函数定义如下:
/**
* 数据包拆分脚本, 从字节流中提取出完整的数据包, 或丢弃部分数据.
*
* @param {Object} client 客户端对象
* @param {Buffer} data 已接收但未处理的数据
* @return {Object}
*/
function handler(client, buffer) {
// 拆包逻辑
return {"package": {"start": 0, "length": 128, "next": 129}, "drop": 0};
}
参数说明
参数名 | 参数类型 | 参数说明 |
---|---|---|
client | object | 客户端连接对象 |
buffer | Buffer | 从客户端接收到的内容 |
返回值说明
- 当字节数组中包含完整数据包时, 返回包含
package
字段的对象. - 如果字节数组中存在错误, 则返回包含
drop
字段的对象, 丢弃掉错误的数据. - 如果字节数组中即不包含完整的数据包也不存在错误, 直接返回
undefined
, 表示需要从客户端接收更多的数据. | 参数名 | 参数类型 | 参数说明 | 示例值 | | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------- | ---------------------------------------- | | package | object | 数据包信息, 如果要丢弃数据该字段不需要返回 | {"start": 0, "length": 128, "next": 129} | | start | 数值 | 数据包在data
| | 中的起始位置 | 4 表示第 5 个字节为数据包的起始位置 | | length | 数值 | 数据包的长度, 表示从start
| | 后多少个字节为一个完整的数据包 | start=4, length=128 表示从第 5 个字节开始(包括第 5 个字节)连续 128 个字节为一个完整的数据包 | | next | 数值 | 下一个数据包的起始位置 | 例如: 在使用\\r\\n
| | 作为数据包间的固定分隔符时, 下一个数据包的起始位置应该为start + length + 2
| | drop | 数值 | 要丢弃的字节数量, 即丢弃字节数组中data[0:drop]
| | 内容 | 128 表示丢弃前 128 个字节. 一般用于数据包错误时丢弃掉错误的数据包场景 |
正常接收数据
当数据包拆分脚本返回以下内容时, 表示已接收到的数据中包含了完整的数据包. 该数据包在所有数据中的起始位置为 0
,
长度为 128
, 下一个数据包的起始位置为 129
{
"package": {
"start": 0,
"length": 128,
"next": 129
}
}
例如, 使用 \r\n
作为数据包间的分隔符, 以解析该数据时 {"a":1,"b":2}\r\n{"a":3,"b":4}\r\n
,
拆分结果为 {"package":{"start":0,"length":13,"next":15}}
.
此时, 数据处理脚本
中接收到的数据为 {"a":1,"b":2}
, 并不会包含 \r\n
,
而下一次数据拆分脚本接收到的数据为 {"a":3,"b":4}\r\n
.
包含错误或不完整数据包时
返回以下内容时, 表示接收到的数据不完整, 需要丢弃 0 - 12 部分内容.
{
"drop": 12
}
例如, 接收到的内容为 :1,"b":2}\r\n{"a":3,"b":4}\r\n
时, :1,"b":2}\r\n
为不完整的数据包需要丢弃,
则需要返回 {"drop":11}
. 下一次数据拆分脚本接收到的数据为 {"a":3,"b":4}\r\n
不包含完整的数据包和错误包时
例如, 接收到的数据为 {"a":1,
未找到分隔符 \r\n
, 此时返回 undefined
表示需要接收更多的数据.
当客户端发送 "b":2}\r\n{"a":3,"b":4}\r\n
数据时, 会再次调用 数据包拆分脚本
, 此时 Buffer
中的数据为 {"a":1,"b":2}\r\n{"a":3,"b":4}\r\n
, 此时可以提取出完整的数据包.
注: 如果返回结果中同时包含了
**drop**
和**package**
字段并且**drop**
的值大于 0 时, 只做丢弃数据处理.
示例
// 校验数据包
function check(data) {
return true;
}
// 使用固定分隔符进行拆包实例逻辑
function handler(client, buffer) {
// 查找分隔符 \r\n
const index = buffer.indexOf("\r\n", 0);
// 如果未找到分隔符, 表示当前字节数组中不包含完整的数据包, 直接返回 undefined
if (index < 0) {
return undefined;
}
// 从 buffer 复制出完整数据包
const pkg = Buffer.alloc(index);
buffer.copy(pkg, 0, 0, index);
// 数据包校验, 如果数据错误可直接返回 drop
if (!check(pkg)) {
return {"drop": index + 2};
}
return {"package": {"start": 0, "length": index, "next": index + 2}};
}
驱动内置数据包拆包函数
- 固定长度头
该函数会取前
N
个字节作为长度信息, 然后根据长度信息读取主体内容. +--------+-----------+ | Length | Data | +--------+-----------+
在数据包拆分脚本中直接使用内置的拆分函数, 如下所示:
// 创建长度头为 4 个字节, 长度头为小端字节序
// 第 1 个参数为长度占用的字节数量
// 第 2 个参数为长度头的字节序, 取值可以为 little 或 big
const handler = createFixedLengthSplitFn(4, 'little');
- 固定分隔符 该函数会使用固定分隔符拆分数据包. 使用方式如下所示:
// 创建使用 \r\n 作为分隔符
const handler = createDelimiterSplitFn('\r\n');
- 固定开始和结束符 该函数会读取指定开始符和结束符之间的数据做为一个完整的数据包. 使用方式如下所示:
// 创建以 '@' 开头并且以 '#' 结尾的数据包拆分函数
const handler = createStartEndDelimiterSplitFn('@', '#');
数据处理脚本
该脚本用于处理 数据包拆分脚本
函数解析得到的完整数据包, 根据协议和数据格式将数据包解析为平台定义的数据格式.
函数定义如下:
/**
* 数据处理脚本, 解析从客户端接收到的数据并转换为平台规定的数据格式
*
* @param {Object} client 客户端对象
* @param {Buffer} 由 '数据包拆分脚本' 拆分得到的完整数据包
* @return {Array} 解析出的采集数据信息
*/
function handler(client, buffer) {
// 数据包处理逻辑
// 如果返回空数组或 undefined, 则表示未解析出任何有效数据
// 返回结果必须为数组, 数组中每个元素为一个设备的实时数据信息
return [
{
"id": "d01", // 设备标识
"time": 1665999863637, // 数据采集时间(ms), unix 时间戳
"values": {"key1": "str", "key2": 123} // 数据点, key 为数据点的标识, value: 为数据点的值
}
];
}
参数说明
参数名 | 参数类型 | 参数说明 |
---|---|---|
client | object | 客户端连接对象 |
buffer | Buffer | 由 splitHandler |
函数得到的数据包 |
返回值
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
array[object] | 对象数组 | 返回值 | [{"id":"d01","time":1665999863637,"values":{"temperature":17.5,"humidity":35.7}}] |
id | 字符串 | 资产编号或设备标识 | d01 |
time | 数值 | 时间戳(ms) | 1664256913000 |
fields | 对象 | 数据点信息 | {"temperature":17.5,"humidity":35.7} |
key | 字符串 | 数据点标识 | "temperature" |
value | any | 数据点的值 | 17.5 |
注: 返回值必须为
**Object[]**
或**undefined**
其中之一. 返回空数组表示未从接收到的数据中解析出有效数据,**undefined**
表示无返回结果, 驱动程序无须处理返回结果. 例如: 接收到的数据为心跳数据, 不包含采集数据.
示例
function handler(client, buffer) {
// 以 json 格式为例, 例如: {"id":"d01","time":"2022-10-17 17:57:32","values":[{"name":"temperature","data":17.5},{"name":"humidity","data":35.7}]}
const jsonData = JSON.parse(buffer.toString());
const time = moment(jsonData.time, "YYYY-MM-DD HH:mm:ss");
const values = {};
for (let i = 0; i < jsonData.values.length; i++) {
const value = jsonData.values[i];
values[value.name] = value.data;
}
return [
{id: jsonData.id, values: values, time: time.valueOf()}
]
}
指令处理脚本
该脚本用于将发送的指令内容转换为字节数组, 当向设备发送指令时, 驱动会将要发送的内容先经过 命令处理脚本
函数处理,
返回结果作为实际发送的内容.
函数定义如下:
/**
* 指令处理脚本, 当发送指令时将指令信息转换为字节数组
*
* @param {object} client 客户端对象
* @param {string} serialNo 平台指令下发序号
* @param {string} deviceId 设备标识
* @param {object} command 指令信息, 详细格式说明见驱动配置文档
* @return {Buffer} 最终发送数据
*/
function handler(client, serialNo, deviceId, command) {
// 数据转换处理, 将待发送内容转换为 Buffer
// 返回结果必须为字节数组
return Buffer.from("hello"); // 表示发送 "hello"
}
参数说明
参数名 | 参数类型 | 参数说明 |
---|---|---|
client | object | 客户端对象 |
serialNo | string | 平台指令下发序号 |
deviceId | string | 自定义设备标识 |
command | object | 指令信息, 格式如下 |
指令格式如下:
以 test
指令为例
{
"name": "test",
"showName": "测试",
"ops": [
{
"value": "123"
}
],
"params": {
"test": {
"attr1": "value1",
"attr2": 123
}
}
}
字段名 | 参数类型 | 参数说明 |
---|---|---|
name | 字符串 | 指令名称 |
showName | 字符串 | 指令显示名称 |
ops.value | 字符串 | 指令中配置的发送内容 |
params | 对象 | 数据写入配置 |
defaultValue | 对象 | 数据写入配置中各字段的默认值 |
注:
ops.value
通常为实际发送的内容, 该字段为必填值.opts
为数组, 目前长度固定为 1.当要发送的数据内容比较复杂时, 可以先转发送内容转换为 base64 格式的字符串, 然后在在指令脚本中再进行 base64 解码后再发送.
返回值说明
必须值必须为 Buffer
对象.
注: 可以使用
Buffer.from("this is a string"")
等方法创建Buffer
对象. 关于Buffer
的使用说明可以点击查看
示例
// 指令发送内容为 base64 格式, 在发送时先进行 base64 解码再发送
function handler(client, serialNo, deviceId, command) {
// 从指令信息中取出要发送的内容, 格式为 base64
const value = data.ops[0].value;
// 将要发送的内容转换为 buffer, 由于指令中的发送内容为 base64, 所在指定编码
return Buffer.from(value, 'hex');
}
连接处理脚本
当客户端连接到驱动或断开时, 会调用 连接处理脚本
.
函数定义如下:
/**
* 连接处理脚本, 当与服务端连接建立或断开时执行的操作
*
* @param {Object} client 客户端对象
* @param {boolean} state 连接状态, true: 已连接, false: 已断开
*/
function handler(client, state) {
if (state) {
// 当客户端连接到驱动时执行操作
} else {
// 当连接断开时执行操作
}
}
参数说明
参数名 | 参数类型 | 参数说明 |
---|---|---|
client | object | 客户端对象 |
state | bool | 连接状态. true: 连接已建立, false: 连接已断开 |
返回值说明
无返回值
示例
function handler(client, state) {
// 当客户端连接到驱动时, 向客户端发送指定数据
if (state) {
client.send(Buffer.from("start report"));
} else {
// 连接断开时, 执行一些操作. 例如: 清理上下文对象等
}
}
设备与连接绑定关系说明
设备与连接关系绑定, 主要用于向设备发送指令时, 驱动会根据该绑定关系确认通过哪个连接发送指令数据. 绑定关系的创建有以下两种:
- 当协议中定义了设备注册功能时, 由
数据处理脚本
函数在接收到设备注册数据包时, 通过调用client
中的register
向驱动注册设备实现设备与连接折绑定. - 当协议中未定义设备注册功能时, 驱动会自动记录每个设备是由哪个连接上报实时数据的(设备每次上传实时数据时都会自动更新), 当驱动下发指令时, 会自动根据该信息向对应的连接发送指令数据.
注: 如果通过
**register**
函数注册了设备时, 会忽略根据实时数据建立的绑定关系. 如果协议中未定义设备注册功能并且设备未上报过实时数据时, 无法通过实时数据建立绑定关系, 此时无法向该设备发送数据